/// File: ViewletControls\lightweightcontrols.fs
/// 
/// Author: Antonio Cisternino (cisterni@di.unipi.it)
/// Author: Giacomo Righetti (righetti@di.unipi.it)
///   
/// ------------------------------------------------------------
/// Copyright (c) Microsoft Corporation.  All rights reserved.
/// 
/// The use and distribution terms for this software are 
/// contained in the file named license.txt, which can be found 
/// in the root of this distribution.
/// By using this software in any fashion, you are agreeing to 
/// be bound by the terms of this license.
///
/// You must not remove this notice, or any other, from this
/// software.
/// ------------------------------------------------------------

namespace LightweightControls

open System
open System.Drawing
open System.Windows.Forms

///
/// Summary:
///     The abstract base class for all lightweight controls.
[<AbstractClass>]
type LightweightControl() =

  let mutable position = new Point()
  let mutable size = new Size()
  let mutable container:Option<LightweightContainer> = None
  let mutable backColor = SystemColors.Control
  let mutable font = SystemFonts.DefaultFont
  let mutable anchor = AnchorStyles.None
  let mutable enabled = true
  let mutable visible = true
  let mutable rightToLeft = RightToLeft.No
  let mutable tabStop = true
  let mutable tabIndex = -1
  let mutable minimumSize = Size(0,0) 
  let mutable maximumSize = Size(0,0)

  let gotFocusEvent = new Event<EventArgs>()
  let lostFocusEvent = new Event<EventArgs>()
  let mouseDownEvent = new Event<MouseEventArgs>()
  let moveEvent = new Event<EventArgs>()
  let clickEvent = new Event<EventArgs>()

  ///
  /// Summary:
  ///     Gets or sets the parent container of the control.
  ///
  /// Returns:
  ///     A LightweightContainer that represents the parent container control of the lightweight control.
  member x.Container
    with get() = container
    and set(p:LightweightContainer) = container <- Some p
  
  ///
  /// Summary:
  ///     Gets or sets the coordinates of the upper-left corner of the control relative 
  ///     to the upper-left corner of its container.
  ///
  /// Returns: 
  ///     The Point that represents the upper-left corner of the control relative 
  ///     to the upper-left corner of its container.
  member x.Position
    with get() = position
    and set(value) = 
      if value <> position then
        let rect = x.Bounds
        position <- value
        // FIX: maybe this is better because of the smaller repaint area.
        //x.Invalidate(rect); x.Invalidate(x.Bounds)
        x.Invalidate(Rectangle.Union(x.Bounds, rect))
        moveEvent.Trigger(new EventArgs())
        
  ///
  /// Summary:
  ///     Gets or sets the height and width of the control.
  ///
  /// Returns:
  ///     The Size that represents the height and width of the control in pixels.
  abstract Size : Size with get, set
  default x.Size
    with get() = size
    and set(value) = 
      if value <> size then
        let rect = x.Bounds
        // contraints on the width/height size are applied only if the corresponding Min/Max property is <> 0
        let w = if minimumSize.Width <> 0 || maximumSize.Width <> 0 then Math.Min(maximumSize.Width,  Math.Max(minimumSize.Width, value.Width)) 
                                                                    else value.Width
        let h = if minimumSize.Height <> 0 || maximumSize.Height <> 0 then Math.Min(maximumSize.Height, Math.Max(minimumSize.Height, value.Height)) 
                                                                      else value.Height
        size <- Size(w,h)
        x.Invalidate(Rectangle.Union(x.Bounds, rect))
        x.OnResize (new EventArgs())
        
  ///
  /// Summary:
  ///     Gets or sets the size that is the lower limit that the control can have.
  ///
  /// Returns:
  ///     An ordered pair of type System.Drawing.Size representing the width and height
  ///     of a rectangle.
  member x.MinimumSize
    with get() = minimumSize
    and set(value) = 
      minimumSize <- value
      let oldWidth = size.Width
      let oldHeight = size.Height
      size.Width <- Math.Max(size.Width, value.Width)
      size.Height <- Math.Max(size.Height, value.Height) 
      if size.Width <> oldWidth || size.Height <> oldHeight then
        x.Invalidate()
        x.OnResize (new EventArgs())
  
  ///
  /// Summary:
  ///     Gets or sets the size that is the upper limit that the control can have.
  ///
  /// Returns:
  ///     An ordered pair of type System.Drawing.Size representing the width and height
  ///     of a rectangle.
  member x.MaximumSize
    with get() = maximumSize
    and set(value) = 
      maximumSize <- value
      let oldWidth = size.Width
      let oldHeight = size.Height
      size.Width <- Math.Min(size.Width, value.Width)
      size.Height <- Math.Min(size.Height, value.Height) 
      if size.Width <> oldWidth || size.Height <> oldHeight then
        x.Invalidate()
        x.OnResize (new EventArgs())
      
  ///
  /// Summary:
  ///     Gets the bounding rectangle of the control.
  ///
  /// Returns:
  ///     The Rectangle relative to the parent control that represents the bounding 
  ///     rectangle of the control.
  member x.Bounds
    with get() = new Rectangle(position, size)
  
  /// <summary>
  /// Hit test used by container to establish if a point is inside the control. By default
  /// bounds are checked but you can override the method and use, for instance the IsVisible
  /// and IsOutlineVisible methods of a GraphicsPath object. This methods allows for controls
  /// with holes in event handling.
  /// </summary>
  abstract member HitTest : int*int -> bool
  default self.HitTest (x, y) =
    self.Bounds.Contains(x,y)
    
  ///
  /// Summary:
  ///     Gets the rectangle that represents the client area of the control.
  ///
  /// Returns:
  ///     The Rectangle that represents the client area of the control.
  member x.ClientRectangle 
    with get() = new Rectangle(Point.Empty, size)  
    
  ///
  /// Summary:
  ///     Gets or sets a value indicating whether the control can respond to user interaction.
  ///
  /// Returns:
  ///     true if the control can respond to user interaction; otherwise, false. 
  ///     The default is true.
  member x.Enabled
    with get() = enabled
    and set(value) = 
      enabled <- value; x.Invalidate()
            
  ///
  /// Summary:
  ///     Gets or sets the width of the control.
  ///
  /// Returns:
  ///     The width of the control in pixels.                        
  member x.Width
    with get() = size.Width
    and set(value) =
      if size.Width <> value then
        let rect = x.Bounds
        size.Width <- if minimumSize.Width <> 0 || maximumSize.Width <> 0 then Math.Min(maximumSize.Width,  Math.Max(minimumSize.Width, value))
                                                                          else value
        x.Invalidate(Rectangle.Union(x.Bounds, rect))
        x.OnResize(new EventArgs())
  
  ///
  /// Summary:
  ///     Gets or sets the height of the control.
  ///
  /// Returns:
  ///     The height of the control in pixels.
  abstract Height : int with get, set
  default x.Height
    with get() = size.Height
    and set(value) = 
      if size.Height <> value then
        let rect = x.Bounds
        size.Height <- if minimumSize.Height <> 0 || maximumSize.Height <> 0 then Math.Min(maximumSize.Height, Math.Max(minimumSize.Height, value))
                                                                             else value
        x.Invalidate(Rectangle.Union(x.Bounds, rect))
        x.OnResize(new EventArgs())

  ///
  /// Summary:
  ///     Gets or sets the distance, in pixels, between the left edge of the control
  ///     and the left edge of its container's client area.
  ///
  /// Returns:
  ///     An System.Int32 representing the distance, in pixels, between the left edge
  ///     of the control and the left edge of its container's client area.
  member x.Left
    with get() = position.X
    and set(l) =
      if position.X <> l then
        let rect = x.Bounds
        position.X <- l
        // FIX: maybe this is better because of the smaller repaint area.
        //x.Invalidate(rect); x.Invalidate(x.Bounds)
        x.Invalidate(Rectangle.Union(x.Bounds, rect))
        moveEvent.Trigger(new EventArgs())

  ///
  /// Summary:
  ///     Gets or sets the distance, in pixels, between the top edge of the control
  ///     and the top edge of its container's client area.
  ///
  /// Returns:
  ///     An System.Int32 representing the distance, in pixels, between the bottom
  ///     edge of the control and the top edge of its container's client area.
  member x.Top
    with get() = position.Y
    and set(t) = 
      let rect = x.Bounds
      position.Y <- t
      // FIX: maybe this is better because of the smaller repaint area.
      // x.Invalidate(rect); x.Invalidate(x.Bounds)
      x.Invalidate(Rectangle.Union(x.Bounds, rect))
      moveEvent.Trigger(new EventArgs())

  ///
  /// Summary:
  ///     Gets the distance, in pixels, between the right edge of the control and the
  ///     left edge of its container's client area.
  ///
  /// Returns:
  ///     An System.Int32 representing the distance, in pixels, between the right edge
  ///     of the control and the left edge of its container's client area.
  member x.Right
    with get() = position.X + size.Width
    and set(r) =
      x.Width <- Math.Max(0, r - position.X)
        
  ///
  /// Summary:
  ///     Gets the distance, in pixels, between the bottom edge of the control and
  ///     the top edge of its container's client area.
  ///
  /// Returns:
  ///     An System.Int32 representing the distance, in pixels, between the bottom
  ///     edge of the control and the top edge of its container's client area.
  member x.Bottom
    with get() = position.Y + size.Height
    and set(b) = 
      x.Height <- Math.Max(0, b - position.Y)
          
  ///
  /// Summary:
  ///     Gets or sets the background color of the control.
  ///
  /// Returns:
  ///     A System.Drawing.Color value representing the background color.
  member x.BackColor
    with get() = backColor
    and set(c) = backColor <- c; x.Invalidate()
  
  ///
  /// Summary:
  ///     Gets or sets the font of the text displayed by the control.
  ///
  /// Returns:
  ///     The System.Drawing.Font to apply to the text displayed by the control. The
  ///     default is the value of the System.Windows.Forms.Control.DefaultFont property.
  member x.Font
    with get() = font
    and set(f) = font <- f; x.Invalidate()

  ///
  /// Summary:
  ///     Gets or sets the edges of the container to which a control is bound and determines
  ///     how a control is resized with its parent.
  ///
  /// Returns:
  ///     A bitwise combination of the System.Windows.Forms.AnchorStyles values. 
  ///     The default is Top and Left.
  member x.Anchor
    with get() = anchor
    and set(a) = anchor <- a
  
  ///
  /// Summary:
  ///     Gets or sets a value indicating whether the control is displayed.
  ///
  /// Returns:
  ///     true if the control is displayed; otherwise, false. The default is true.
  member x.Visible
    with get() = visible
    and set(value) = 
      visible <- value; x.Invalidate()
  
  ///
  /// Summary:
  ///     Gets or sets a value indicating whether control's elements are aligned to
  ///     support locales using right-to-left fonts.
  ///
  /// Returns:
  ///     One of the System.Windows.Forms.RightToLeft values. 
  ///     The default is System.Windows.Forms.RightToLeft.Inherit.
  member x.RightToLeft
    with get() = rightToLeft
    and set(value) = 
      rightToLeft <- value; 
      x.Invalidate()
 
  ///
  /// Summary:
  ///     Gets or sets a value indicating whether the user can give the focus to this control using the TAB key.
  ///
  /// Returns:
  ///     true if the user can give the focus to the control using the TAB key; otherwise, false. The default is true.
  abstract TabStop : bool with get, set
  default x.TabStop
    with get() = tabStop
    and set(value) = tabStop <- value
 
  ///
  /// Summary:
  ///     Gets or sets the tab order of the control within its container.
  ///
  /// Returns:
  ///     The index value of the control within the set of controls within its container.
  ///     The controls in the container are included in the tab order.
  member x.TabIndex
    with get() = tabIndex
    and set(value) = tabIndex <- value
 
  ///
  /// Summary:
  ///     Gets or sets the cursor that is displayed when the mouse pointer is over
  ///     the control.
  ///
  /// Returns:
  ///     A System.Windows.Forms.Cursor that represents the cursor to display when
  ///     the mouse pointer is over the control.
  member x.Cursor
    with get() = if container.IsSome then container.Value.Cursor else null
    and set(c:Cursor) = 
      if container.IsSome then
        container.Value.Cursor <- c
        
  ///
  /// Summary:
  ///     Gets a value indicating whether the control has input focus.
  ///
  /// Returns:
  ///     true if the control has focus; otherwise, false.
  member x.Focused = 
    if container.IsSome then container.Value.HasFocus(x) else false
  
  ///
  /// Summary:
  ///     Gets a value indicating whether the control is a sort of "container" 
  ///     for other controls, i.e: panel, groupbox, etc.
  ///
  /// Returns:
  ///     true if the control is a "container"; otherwise, false.
  abstract IsContainer : bool 
  default x.IsContainer = false
  
  member x.GotFocus = gotFocusEvent.Publish
  
  member x.LostFocus = lostFocusEvent.Publish
  
  member x.MouseDown = mouseDownEvent.Publish

  member x.Move = moveEvent.Publish
  
  member x.Click = clickEvent.Publish
  
  ///
  /// Summary:
  ///     Sets input focus to the control.
  member x.Focus () = 
    if container.IsSome then container.Value.SetFocus(x) 
  
  ///
  /// Summary:
  ///     Invalidates the entire surface of the control and causes the control to be
  ///     redrawn.
  member x.Invalidate() =
    if container.IsSome then
      container.Value.Invalidate(x.Bounds)
  
  ///
  /// Summary:
  ///     Invalidates the specified region of the control and causes the control to be
  ///     redrawn.         
  member x.Invalidate(r:Rectangle) =
    if container.IsSome then
      container.Value.Invalidate(r)
  
  member internal x.InternalPaint (e:PaintEventArgs) =
    if visible then
      let g = e.Graphics
      let gs = g.Save()
      let r = x.ClientRectangle
      
      g.TranslateTransform(float32 position.X, float32 position.Y)
      g.Clip <- new Region(r)
      x.OnPaint(new PaintEventArgs(g, r))
      g.Restore(gs)
  
  abstract OnPaint : PaintEventArgs -> unit

  member internal x.InternalMouseDown (e:MouseEventArgs) =
    let evt = new MouseEventArgs(e.Button, e.Clicks, e.X - position.X, e.Y - position.Y, e.Delta)
    x.OnMouseDown (evt)

  member internal x.InternalMouseUp (e:MouseEventArgs) =
    let evt = new MouseEventArgs(e.Button, e.Clicks, e.X - position.X, e.Y - position.Y, e.Delta)
    x.OnMouseUp (evt)

  member internal x.InternalMouseMove (e:MouseEventArgs) =
    let evt = new MouseEventArgs(e.Button, e.Clicks, e.X - position.X, e.Y - position.Y, e.Delta)
    x.OnMouseMove (evt)
  
  ///
  /// Summary:
  ///     Brings the control to the front of the z-order.
  member x.BringToFront () =
    if container.IsSome then
      container.Value.BringChildToFront(x)
  
  ///
  /// Summary:
  ///     Sends the control to the back of the z-order.
  member x.SendToBack () =
    if container.IsSome then
      container.Value.SendChildToBack(x)
  
  abstract OnClick : EventArgs -> unit
  default x.OnClick (e:EventArgs) = clickEvent.Trigger(EventArgs.Empty)

  abstract OnMouseDown : MouseEventArgs -> unit
  default x.OnMouseDown (e:MouseEventArgs) = ()

  abstract OnMouseUp : MouseEventArgs -> unit
  default x.OnMouseUp (e:MouseEventArgs) = ()

  abstract OnMouseMove : MouseEventArgs -> unit
  default x.OnMouseMove (e:MouseEventArgs) = ()

  abstract OnMouseEnter : EventArgs -> unit
  default x.OnMouseEnter (e:EventArgs) = ()

  abstract OnMouseLeave : EventArgs -> unit
  default x.OnMouseLeave (e:EventArgs) = ()

  abstract OnKeyUp : KeyEventArgs -> unit
  default x.OnKeyUp (e:KeyEventArgs) = ()

  abstract OnKeyDown : KeyEventArgs -> unit
  default x.OnKeyDown (e:KeyEventArgs) = ()
     
  abstract OnResize : EventArgs -> unit
  default x.OnResize (e:EventArgs) = ()

  abstract OnLostFocus : EventArgs -> unit
  default x.OnLostFocus (e:EventArgs) = lostFocusEvent.Trigger(EventArgs.Empty)
  
  abstract OnGotFocus : EventArgs -> unit
  default x.OnGotFocus (e:EventArgs) = gotFocusEvent.Trigger(EventArgs.Empty)
  
///
/// Summary:
///     This class acts as a container for the lightweight controls.
and LightweightContainer() as x =
  inherit UserControl()
  
  let controls = new System.Collections.Generic.List<LightweightControl>()
 
  let mutable oldClicked = -1 // used to keep track the last control that was clicked
  let mutable over = -1
  let mutable focus = -1
  
  let mutable nextTabIndex = 0
  let mutable showTabIndexedControl = false
  
  do
    x.SetStyle(ControlStyles.AllPaintingInWmPaint ||| ControlStyles.OptimizedDoubleBuffer, true)
  
  member internal x.ShowTabIndexedControl
    with get() = showTabIndexedControl
  
  ///
  /// Summary:
  ///     Adds the specified lightweight control to the controls collection.
  member x.Add (c:#LightweightControl) =
    if c.IsContainer then controls.Insert(0, c) else controls.Add(c)
    c.Container <- x
    // assign tab index for the control if it doesn't have one.
    if c.TabIndex = -1 then 
      c.TabIndex <- nextTabIndex
      nextTabIndex <- nextTabIndex + 1
  
  ///
  /// Summary:
  ///     Adds a list of lightweight controls to the controls collection.
  member x.AddRange (l:#LightweightControl list) =
    List.iter x.Add l
           
  ///
  /// Summary
  ///     Removes the specified lighweight control from the controls collection.
  member x.Remove (c:#LightweightControl) =
    x.RemoveAt(controls.IndexOf(c))
    
  ///
  /// Summary
  ///     Removes the lighweight control at the specified index from the controls collection.
  member x.RemoveAt (index) =
    let c = controls.[index]
    controls.RemoveAt(index)
    x.Invalidate(c.Bounds)
    x.SelectNextControlTabOrder()
  
  ///
  /// Summary:
  ///     Gets the collection of lightweight controls contained within the control.
  ///
  /// Returns:
  ///     A System.Collections.Generic.List<LightweightControl> representing the collection
  ///     of lightweight controls contained within the control.
  member x.LightweightControls
    with get() = controls
  
  ///
  /// Summary:
  ///     Finds out which control is at position (x,y)
  ///
  /// Returns:
  ///     The index of the child control or -1 is no control is found at the specified position.
  member this.Correlate (x,y) =
    controls.FindLastIndex(fun ctrl -> ctrl.Visible && ctrl.HitTest(x,y))
  
  /// 
  /// Summary:
  ///     Brings the specified child control to the front of the z-order. 
  member x.BringChildToFront (child) = 
    let childIndex = controls.IndexOf(child)
    controls.RemoveAt(childIndex)
    controls.Add(child)
    // updates over and focus.
    if childIndex = over then 
      over <- controls.Count - 1
      if focus = childIndex then focus <- over
            
  /// Summary:
  ///     Brings the specified child control to the back of the z-order.
  member x.SendChildToBack (child) =
    let childIndex = controls.IndexOf(child)
    controls.RemoveAt(childIndex)
    controls.Insert(0, child)
    // updates over and focus.
    if childIndex = over then 
      over <- 0
      if focus = childIndex then focus <- over

  ///
  /// Summary:
  ///     Sets the focus on the specified control.
  //[<OverloadID("SetFocus_LightweightControl")>]
  member x.SetFocus (child: LightweightControl) = 
    x.SetFocus(controls.IndexOf(child))

  ///
  /// Summary:
  ///     Sets the focus on the control with the specified index.
  //[<OverloadID("SetFocus_int")>]
  member x.SetFocus (childIndex : int) =
    let oldFocus = focus
    focus <- childIndex
    if oldFocus <> -1 then
      x.Invalidate(controls.[oldFocus].Bounds)
      controls.[oldFocus].OnLostFocus(EventArgs.Empty)
    x.Invalidate(controls.[focus].Bounds)
    controls.[focus].OnGotFocus(EventArgs.Empty)
  
  /// Summary:
  ///     Determines if the specified control has input focus.
  ///
  /// Returns:
  ///     true if the child control has input focus or false otherwise.
  member x.HasFocus(child) : bool =
    focus = controls.IndexOf(child)

  ///
  /// Summary:
  ///     Selects the next tab indexed control.
  member x.SelectNextControlTabOrder() =
    let oldTabIndex = controls.[focus].TabIndex
    let oldFocus = focus      
    let mutable next = Int32.MaxValue, focus
    let mutable lower = Int32.MaxValue, -1
        
    for i = 0 to controls.Count - 1 do
      let c = controls.[i]
      if c.TabStop then
        if c.TabIndex < fst lower then lower <- c.TabIndex, i
        if c.TabIndex > oldTabIndex && c.TabIndex < fst next then next <- c.TabIndex, i
    
    x.SetFocus(if fst next = Int32.MaxValue then snd lower else snd next)
     
  member x.PaintChildrenControls (e:PaintEventArgs) =
    if controls.Count > 0 then
      for i = 0 to controls.Count - 1 do
        controls.[i].InternalPaint (e)
    
  override x.OnMouseDown (e:MouseEventArgs) =
    base.OnMouseDown(e)
    if over <> -1 && controls.[over].Enabled then
      if controls.[over].TabStop then
        x.SetFocus(over)
        // FIX: necessary because container controls like panel, groupbox are not really "containers" yet!
        //      i.e routing of internal events is not implemented (they will be subclassed of LightweightContainer)
        //      so it necessary to explicitly save the control that is clicked to raise an OnMouseUp event later
        oldClicked <- over
      controls.[over].InternalMouseDown (e)  

  override x.OnMouseUp (e:MouseEventArgs) =
    base.OnMouseUp(e)
    if over <> -1 then
      controls.[over].InternalMouseUp (e)
    // FIX: necessary because container controls like panel, groupbox are not really "containers" yet!
    //      i.e routing of internal events is not implemented (they will be subclassed of LightweightContainer)
    //      so a call to InternalMouseUp must be esplicitly made on the control that was clicked.
    if oldClicked <> -1 then
      controls.[oldClicked].InternalMouseUp (e)
      oldClicked <- over

  override x.OnMouseMove (e:MouseEventArgs) =
    base.OnMouseMove(e)
    let oldOver = over
    over <- x.Correlate (e.X, e.Y)    
    if oldOver <> over then
      if over <> -1 then
        controls.[over].OnMouseEnter(new EventArgs())
      if oldOver <> -1 then
        controls.[oldOver].OnMouseLeave(new EventArgs())  
    if over <> -1 then
      controls.[over].InternalMouseMove (e)

  override x.OnClick (e:EventArgs) =
    base.OnClick(e)
    if over <> -1 then
      controls.[over].OnClick (e)
  
  override x.OnKeyDown (e:KeyEventArgs) =
    base.OnKeyDown(e)
    if e.KeyCode = Keys.Tab then 
      showTabIndexedControl <- true
      x.SelectNextControlTabOrder()
    if focus <> -1 && controls.[focus].Enabled then
      controls.[focus].OnKeyDown (e)

  override x.OnKeyUp (e:KeyEventArgs) = 
    base.OnKeyUp(e)
    if focus <> -1 then
      controls.[focus].OnKeyUp (e)
      
  override x.OnPaint (e:PaintEventArgs) =
    x.PaintChildrenControls(e)